#!/usr/bin/env python
# encoding: utf-8
"""
@author: jfjiang6
@file: pos_tag_n_gram_nltk.py

词性标注，或POS(Part Of Speech)，是一种分析句子成分的方法，通过它来识别每个词的词性。
使用 NLTK 中n-gram语言模型进行中英文词性标注
# n-gram语言模型 包括：
# unigram模型（一元）：一个词的出现与它周围的词是独立
# bigram模型（二元）：一个词的出现仅与它前面的一个词有关时
# ...

"""

import nltk
# pip install nltk
# nltk.download()  # 用来下载下面所需的nltk相关的语料/模型（brown、pumt等)，在出现的界面中选择所需进行下载，已下载忽略
import pickle
import json
from nltk.corpus import brown  # 加载nltk中brown布朗英文语料库
import warnings
warnings.filterwarnings('ignore')


# #############################################################################################
# 步骤1 todo: 利用 unigram 模型对nltk中brown布朗英文语料库进行词性标注及评价
# 步骤1.1 todo: 利用UnigramTagger，并提供词表模型构建标注器，称为查询标注器
# 步骤1.1.1 todo: 统计部分高频词的词性（经常出现的100个词的词性），利用单个词的词性来进行标注

freq_dist = nltk.FreqDist(brown.words(categories='humor'))  # 统计布朗语料库中humor类别中每个词的词频
common_words = freq_dist.most_common(100)  # 选取经常出现的100个词的词性

# 步骤1.1.2  todo: 构建词表
# 对标记后的文本进行条件频率分布，这样可以找到指定单词最多的标记是哪一个
# brown下载路径：C:\Users\Mr.Charley\AppData\Roaming\nltk_data\corpora
con_freq_dist = nltk.ConditionalFreqDist(brown.tagged_words(categories='humor'))
# brown语料库中共包含15个分类：'adventure'（冒险）、‘belles_lettres’(纯文学)、'editorial'（编辑）, 'fiction'（小说）,
# 'government'（政府）, 'hobbies'（爱好）, 'humor'（幽默）, 'learned'（学术）, 'lore'（知识）, 'mystery'（推理小说）, 'news'（新闻）, 'religion', 'reviews'（评论）, 'romance'（浪漫）, 'science_fiction'（科幻小说），
# 找出最常用的100个单词的最多标记，构成词表（单词-标记 字典）
table = dict((word, con_freq_dist[word].max()) for (word, _) in common_words)

# 步骤1.1.3 todo: 使用词表(单词-标记)作为模型，构建查询标注器，进行标注
# 设置回退标注器（默认的标注器），即UnigramTagger无法标注时，会使用默认的标注器，可以提高标注正确率
# DefaultTagger('NN') 默认标注器，默认标注为名词
uni_tagger = nltk.UnigramTagger(model=table, backoff=nltk.DefaultTagger('NN'))
tagged_sents = brown.tagged_sents(categories='humor')

# 步骤1.1.4 todo: 对标注器进行标注评价
print('查询标注器对Brown的humor类别语料的标注正确率为：%0.3f' % uni_tagger.evaluate(tagged_sents))

# #############################################################################################
# 步骤1.2 todo: 利用UnigramTagger，用训练数据构建标注器，称为一元标注器
# 步骤1.2.1 todo: 将tagged_sents划分为训练集和测试集，进行一元语言标注器训练和评价
train_size = 0.67
train_num = int(len(tagged_sents) * train_size)
x_train = tagged_sents[0:train_num]
x_test = tagged_sents[train_num:]
# 步骤1.2.2 todo: 用训练数据构建标注器及评价
uni_tagger_split = nltk.UnigramTagger(train=x_train)

# 步骤1.2.3 todo: 对一元标注器进行标注评价
print('一元标注器UnigramTagger在训练集上标注的正确率为：%0.3f' % uni_tagger_split.evaluate(x_train))
print('一元标注器UnigramTagger在测试集上标注的正确率为：%0.3f' % uni_tagger_split.evaluate(x_test))


# #############################################################################################
# 步骤2 todo: 利用 Bigram 模型对nltk中brown布朗英文语料库进行词性标注
# BigramTagger(train=None, model=None, backoff=None)
# 构建二元语言标注器，同样可以设置回退标注器，另外回退标注器自身也可以有回退标注器，这样来提高标注准确率
# 构建过程如下（称之为组合标注器）：
# 1）优先使用bigram标注器标注单词；
# 2）若bigram标注器无法标记，则使用unigram标注器；
# 3）若unigram标注器无法标记，使用正则化标注器；

# 步骤2.1 todo: 构建英文语料标注的正则化标注器，用做unigram标注器的回退标注器
pattern = [(r'.*ing$', 'VBG'),
           (r'.*ed$', 'VBD'),
           (r'.*es$', 'VBZ'),
           (r'.*\'s$', 'NN$'),
           (r'.*s$', 'NNS'),
           (r'.*', 'NN')  # 未匹配的仍标注为NN
           ]

# 步骤2.2 todo: 用训练数据构建组合标注器
tagger_regexp = nltk.RegexpTagger(pattern)
tagger_Unigram = nltk.UnigramTagger(x_train, backoff=tagger_regexp)
tagger_Bigram = nltk.BigramTagger(x_train, backoff=tagger_Unigram)

# 步骤2.3 todo: 对组合标注器进行标注评价
print('组合标注器在训练集上标注的正确率为：%0.3f' % tagger_Bigram.evaluate(x_train))
print('组合标注器在测试集上标注的正确率为：%0.3f' % tagger_Bigram.evaluate(x_test))

# 步骤2.4 todo: 保存标注器
with open('tagger_Bigram.pkl', 'wb') as tagger:
    pickle.dump(tagger_Bigram, tagger)

# # 步骤2.5 todo: 加载标注器
# with open('tagger_Bigram.pkl', 'rb') as tagger:
#     tagger_Bigram = pickle.load(tagger)


# #############################################################################################
# 步骤3 todo: 利用 nltk 中的 Unigram 、BigramTagger和TrigramTagger构建中文词性标注器
# 从上面可以看出，不需要任何的语言学知识，只需要借助统计数据便可以使得词性标注做的足够好。
# 这里中文语料使用人民日报1998年1月的词性标注语料

# 步骤3.1 todo: 加载人民日报1998年1月的标注文件
lines = open('./data/corpus/pku98/199801.txt', 'rb').readlines()
tagged_sents_all = []

for line in lines:
    line = line.decode('utf-8')
    sent = line.split() # 根据任意空白字符（空格、换行符、制表符等）
    tagged_sent = []
    for item in sent:
        pair = nltk.str2tuple(item) # 将字符串按照/分割成元组的元素
        tagged_sent.append(pair) # 列表中每个元素为每个单词及其词性

    if len(tagged_sent) > 0:
        tagged_sents_all.append(tagged_sent)

# 步骤3.2 todo: 将 tagged_sents_all 划分为训练集和测试集
train_size = 0.67
train_num = int(len(tagged_sents_all) * train_size)
x_train = tagged_sents_all[0:train_num]
x_test = tagged_sents_all[train_num:]

# 步骤3.3 todo: 利用训练集构建一元标注器、二元标注器和三元标注器，并设置回退标注器
tagger_Unigram_cn = nltk.UnigramTagger(train=x_train, backoff=nltk.DefaultTagger('n'))
tagger_Bigram_cn = nltk.BigramTagger(train=x_train, backoff=tagger_Unigram_cn)
tagger_Trigram_cn = nltk.TrigramTagger(train=x_train, backoff=tagger_Bigram_cn)

# 步骤3.4 todo: 对标注器进行标注评价
print('一元标注器UnigramTagger在中文训练集上标注的正确率为：%0.3f' % tagger_Unigram_cn.evaluate(x_train)) # 准确率=预测正确的标签数/总标签数
print('一元标注器UnigramTagger在中文测试集上标注的正确率为：%0.3f' % tagger_Unigram_cn.evaluate(x_test)) # 准确率=预测正确的标签数/总标签数

print('二元标注器BigramTagger在中文训练集上标注的正确率为：%0.3f' % tagger_Bigram_cn.evaluate(x_train))
print('二元标注器BigramTagger在中文测试集上标注的正确率为：%0.3f' % tagger_Bigram_cn.evaluate(x_test))

print('三元标注器TrigramTagger在中文训练集上标注的正确率为：%0.3f' % tagger_Trigram_cn.evaluate(x_train))
print('三元标注器TrigramTagger在中文测试集上标注的正确率为：%0.3f' % tagger_Trigram_cn.evaluate(x_test))

# 步骤3.5 todo: 保存标注器
with open('tagger_Unigram_cn.pkl', 'wb') as tagger:
    pickle.dump(tagger_Unigram_cn, tagger)
with open('tagger_Bigram_cn.pkl', 'wb') as tagger:
    pickle.dump(tagger_Bigram_cn, tagger)
with open('tagger_Trigram_cn.pkl', 'wb') as tagger:
    pickle.dump(tagger_Trigram_cn, tagger)

# 步骤3.6 todo: 利用训练出来的标注器进行测试，并显示标注结果
# word_tokenize用于将一段文本拆分为一个个独立的词或标点符号（即“词元”，token）
tokens = nltk.word_tokenize(u'NLP 作为 AI 技术 领域 中 重要 的 分支 ， 随着 其 技术 应用 范围 不断 扩大 ，'
                            u'在 数据 处理 领域 占有 越来越 重要 的 地位 。')
tagged_1 = tagger_Unigram_cn.tag(tokens) # 对输入的词性序列进行词性标注
tagged_2 = tagger_Bigram_cn.tag(tokens)
tagged_3 = tagger_Trigram_cn.tag(tokens)

print('一元标注器的标注结果：')
print(json.dumps(tagged_1,  ensure_ascii=False))
print('二元标注器的标注结果：')
print(json.dumps(tagged_2,  ensure_ascii=False))
print('三元标注器的标注结果：')
print(json.dumps(tagged_3,  ensure_ascii=False))
